Skip to content

缓存菜品、套餐

  1. 需求:刚刚做的C端商品浏览功能的四个接口,是用户端访问频率最高的(每次点开APP首页都会调用)。
  2. 建议:考虑引入Redis缓存。
  3. 理由:菜品分类和套餐信息不经常变动,直接查询数据库会产生不必要的IO开销。使用Redis缓存这些List结果,可以极大地提升首页加载速度。

1.1 缓存菜品

  1. 目前的难点:在Redis中以什么数据类型(String (❌️) Hash List Set Zset,如果可以组合,Hash来存DishVO,然后再统一由一个List存起来就好了)缓存这些菜品(List<DishVO>),缓存的有效期设置成多少。
  2. 大致流程:先去Redis中查,如果没有,再去MySQL中查,再同步写入到Redis中,并且为了防止缓存雪崩,各个菜品的有效期需要加随机数,防止菜品在Redis中同一时刻全部失效。再延伸到某个极热点的数据,需要设置永久ttl或者逻辑永久。再思考:系统的并发量实际上达不到这样的级别,但是如果成本不高,可以防患于未然。
  3. 需要注意的问题:当增删改菜品时,需要对Redis的数据同步修改

AI review:

维度你的思路优化建议
数据类型倾向于 Hash/List 组合String (JSON),将 List<DishVO> 直接序列化为 JSON 字符串,以分类 ID 为 Key,简单高效。
更新策略同步修改 Redis删除 Redis 对应 Key,让下一次请求触发回写。
一致性仅修改菜品时考虑删除、状态切换(起售/停售) 均需清理缓存。
实现方式手动硬编码逻辑推荐尝试 Spring Cache 注解,事半功倍。
  1. 新的难点:如何将菜品数据序列化/反序列化

    • 这里我们在做Redis配置类的时候,已经配置了Redis的序列化器

      java
              // 设置redis key的序列化器
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
      
              // 设置value的序列化器,使用 GenericJackson2JsonRedisSerializer
              GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
              redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
              redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
    • Redis的序列化器会自动帮我们做好Java对象的序列化与反序列化,所以直接取用即可

    • 目前的问题,无法处理LocalDateTime这类Java8新类型

      修改:

      java
              // 1. 使用自定义对象映射器
              JacksonObjectMapper jacksonObjectMapper = new JacksonObjectMapper();
              // 2. 关键:虽然你的类处理了时间,但为了让 Redis 能反序列化回 List<DishVO>,
              // 依然需要开启类型保留(Type Id)
              jacksonObjectMapper.activateDefaultTyping(
                      LaissezFaireSubTypeValidator.instance,
                      ObjectMapper.DefaultTyping.NON_FINAL,
                      JsonTypeInfo.As.PROPERTY
              );
      
              // 3. 将对象映射器传给序列化器
              GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(jacksonObjectMapper);
      
      
              // 设置redis key的序列化器
              redisTemplate.setKeySerializer(new StringRedisSerializer());
              redisTemplate.setHashKeySerializer(new StringRedisSerializer());
      
              // 设置value的序列化器,使用 GenericJackson2JsonRedisSerializer
              redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer);
              redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer);
      
              return redisTemplate;
  2. 实现增删改菜品操作时清理Redis缓存

    • 增加菜品

      java
      redisTemplate.delete("dish_" + dishDTO.getCategoryId());
    • 删除菜品

      java
      redisTemplate.delete(redisTemplate.keys("dish_*"));
    • 修改菜品时就要注意了,如果修改的是菜品的分类,那么原分类的缓存和新分类的缓存都需要清掉

      这里不需要做这样的逻辑判断,直接全部清除掉即可

1.2 缓存套餐

1.2.1 Spring Cache

  1. Spring Cache,是一个框架,对缓存技术的封装,实现以注解的形式实现缓存功能。底层可以切换不同的缓存实现,如:EHCache、Caffeine、Redis。

  2. 使用

    • 导入Maven依赖,根据你导入的缓存技术的依赖,决定其底层具体实现的缓存技术。比如你导入的是Redis的坐标,导入Spring Cache的坐标后,其底层实现的就是Redis。

      xml
      <!-- Spring Cache -->
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-cache</artifactId>
          <version>2.7.3</version>
      </dependency>
    • 常用注解:

      注解作用对应 Redis 操作典型应用场景
      @EnableCaching开启缓存注解功能,通常加在启动类或Redis配置类上
      @Cacheable查缓存:有则返回,无则执行方法并存入缓存GET + SETC 端查询菜品/套餐列表
      @CachePut更新缓存:执行方法并将结果强制放入缓存SET管理端修改菜品后同步更新
      @CacheEvict清理缓存:从缓存中删除指定数据DEL管理端删除菜品或停售菜品
      @Caching组合注解:可以同时触发多个缓存操作组合操作一个操作影响多个分类缓存时
      @CacheConfig类级共享配置:抽取统一的缓存名称N/A在类头声明 setmealCache
      • 注解属性值: cacheNames = "userCache",key = "#result.id"。最后存在Redis中的key为 userCache::id

      • key属性的值有多种写法,使用spel (Spring Expression Language)

        • key = "#user.id" ,user 表示入参,名称需要和参数名保持一致

        • key = "#result.id",result 表示返回值,固定写法

        • key = "#p0.id",p0 表示第一个参数,p1 表示第二个参数

        推荐使用第一种,可读性高。==注意:不同的注解支持key属性的写法不同==,比如@Cacheable就不支持result写法

    • 写法:

      java
      @Cacheable(cacheNames = "userCache",key = "#id")
      java
      @CachePut(cacheNames = "userCache",key = "#user.id")
      java
      @CacheEvict(cacheNames = "userCache",key = "#id")
      java
      @CacheEvict(cacheNames = "userCache",allEntries = true) // 清理所有userCache开头的key

1.2.2 实现思路

  1. 导入Maven坐标
  2. 在Redis配置类中添加Spring Cache管理器,指定Spring Cache序列化和反序列化的规则
  3. 启用@EnableCaching
  4. 在用户端查询套餐接口上加上@Cacheable注解
  5. 在管理端增删改套餐接口上加上@CachePut、@CacheEvict注解

1.2.3 开发

  1. 导入Maven坐标

  2. 配置Spring Cache管理器,在原来的基础上添加CacheManager方法,返回该Bean。并启用SpringCache

    java
    /**
     * redis配置类
     */
    @EnableCaching
    @Configuration
    @Slf4j
    public class RedisConfiguration {
    
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
            log.info("开始构建Redis缓存管理器...");
            // 1. 构造和你 redisTemplate 一模一样的 ObjectMapper
            JacksonObjectMapper jacksonObjectMapper = new JacksonObjectMapper();
            jacksonObjectMapper.activateDefaultTyping(
                    LaissezFaireSubTypeValidator.instance,
                    ObjectMapper.DefaultTyping.NON_FINAL,
                    JsonTypeInfo.As.PROPERTY
            );
    
            // 2. 使用这个 ObjectMapper 创建序列化器
            GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(jacksonObjectMapper);
    
            // 3. 配置 RedisCacheConfiguration
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                    // 设置 Key 的序列化方式为 String
                    .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
                    // 设置 Value 的序列化方式为 JSON(使用你自定义的规则)
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                    // 【建议】设置默认过期时间,例如 1 小时,防止缓存永久堆积
                    .entryTtl(Duration.ofHours(1))
                    // 不缓存空值
                    .disableCachingNullValues();
    
            // 4. 构建并返回 CacheManager
            return RedisCacheManager.builder(redisConnectionFactory)
                    .cacheDefaults(config)
                    .build();
        }
    }
  3. 修改用户端,注意:Spring Cache注解一般写在Service层,可以保证业务逻辑的复用性。但有需要注意的"==大坑==",==类内部方法调用导致缓存失效==。

    • 原因:Spring Cache 是基于 动态代理 的。只有当外部对象调用 Service 时,才会经过 Spring 的代理对象,从而触发缓存逻辑。如果是“类内部自调用”,执行的是原始对象的方法,注解会直接被忽略。

    • 错误示例:

      java
      @Service
      public class DishServiceImpl implements DishService {
      
          public List<DishVO> getById(Long id) {
              // 直接调用本类带注解的方法
              return this.listWithCache(id); // ❌ 此时 @Cacheable 会失效!
          }
      
          @Cacheable(cacheNames = "dish", key = "#id")
          public List<DishVO> listWithCache(Long id) {
              return dishMapper.getById(id);
          }
      }
  4. 在进行管理端套餐开发的时候,遇到了一个很隐蔽的问题,经过排查,发现在保存套餐的时候,上一步向套餐表插入信息的时候,没有进行主键回填,导致插入套餐对应菜品的时候,套餐ID为空。新增套餐时进行主键回填,解决了新增套餐插入套餐菜品映射表的问题。

  5. 现在又遇到问题,修改套餐表的时候,前端并未传入菜品套餐表的套餐ID,需要手动设置,已成功解决。

  6. 现在进行套餐缓存开发,将用户端套餐查询操作@Cacheable打开,客户端增删改操作同步修改缓存

  7. 问题:客户端增删改操作对缓存操作的粒度如何控制